Skip to content

Restore #185: verify a Cloudinary media url is our asset - #188

Merged
renrenmimi merged 1 commit into
mainfrom
restore/media-url-ownership
Sep 6, 2026
Merged

Restore #185: verify a Cloudinary media url is our asset#188
renrenmimi merged 1 commit into
mainfrom
restore/media-url-ownership

Conversation

@renrenmimi

Copy link
Copy Markdown
Owner

Reverts the revert in 72c8efe. That rollback was made on the wrong diagnosis — #185 was blamed for the upload outage, and it never touched media.ts or the client upload path. The actual cause was max_file_size in the signed parameter set, fixed in #187.

#185 fixes a real vulnerability, so dropping it was a loss with no compensating gain.

What comes back

validateTrustedHttpsUrl checked protocol and hostname and nothing else. TRUSTED_MEDIA_URL_HOSTS is ["res.cloudinary.com"] — which proves the bytes are served by Cloudinary, not that they are ours. Anyone can register a free Cloudinary account, so https://res.cloudinary.com/<their-cloud>/... passed unchanged, letting a client:

  • skip the whole upload pipeline (size caps, per-user folder, signature rate limit) by never requesting a signature
  • keep ownership of the bytes, so anything that survived moderation could be swapped later at the same url, on every post, avatar, cover image and check-in referencing it

Restored: the cloud-name + petnote/ folder check, the eleven secrets: [CLOUDINARY_CLOUD_NAME] bindings, and media-url-ownership.test.ts (7 tests, 4 of which fail without the fix).

Verified present after the revert-of-revert: assertOwnCloudinaryAsset and CLOUDINARY_HOST in shared.ts, 11 secret bindings across pets.ts (2), meetups.ts (2), places.ts (4), users.ts (2), posts.ts (1), and the test file. Local build / lint / typecheck:test clean.

Sequencing

Deliberately a separate PR from #187, and deliberately merged straight after it, so one deploy carries both and one real upload test covers both. #187 makes uploads work; this makes the resulting url actually get checked.

Note

#185's own PR flagged that the eleven bindings were derived by grepping every call path, not by testing — the emulator suite drives handlers through .run(), which bypasses secret mounting, so a missed binding passes CI and throws internal in production. That caveat still stands and is the reason the post-deploy upload test matters. Task 4 removes this whole failure mode by making the cloud name a plain constant rather than a secret; that lands next.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 6, 2026 06:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
pet-note Ready Ready Preview Sep 6, 2026 6:24am UTC

@renrenmimi
renrenmimi merged commit c485ac9 into main Sep 6, 2026
6 checks passed
@renrenmimi
renrenmimi deleted the restore/media-url-ownership branch September 6, 2026 06:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c99e86cdc5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread functions/src/shared.ts
Comment on lines +198 to +199
!parsed.pathname.startsWith(`/${cloudName}/`) ||
!parsed.pathname.includes(`/${CLOUDINARY_FOLDER}/`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict URLs to Cloudinary upload delivery paths

A client can still bypass the upload pipeline with a Cloudinary remote-fetch URL such as https://res.cloudinary.com/<our-cloud>/image/fetch/https://attacker.example/petnote/image.jpg: its pathname starts with our cloud name and contains /petnote/, so every affected media-writing callable accepts it, while Cloudinary serves bytes fetched from an attacker-controlled origin. Validate the Cloudinary resource/delivery segments (for example, require the expected image|video/upload form) and locate the folder in the public-ID portion rather than anywhere in the pathname.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants